/*
 * MIT License
 *
 * Copyright (c) 2019 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package cn.seaboot.commons.digest;

import java.nio.charset.Charset;

/**
 * Static methods for translating Base64 encoded strings to byte arrays and vice-versa.
 *
 * @author Mr.css
 * @since 1.4
 */
public class Base64 {
    private Base64() {
    }

    /**
     * Translates the specified byte array into a Base64 string as per Preferences.put(byte[]).
     *
     * @param bytes data
     * @return base64
     */
    public static byte[] encode(byte[] bytes) {
        return encode(bytes, ENCODE_TABLE);
    }

    /**
     * Translates the specified byte array into a Base64 string as per Preferences.put(byte[]).
     *
     * @param bytes data
     * @return base64
     */
    public static String encodeString(byte[] bytes) {
        return new String(encode(bytes, ENCODE_TABLE), Charset.defaultCharset());
    }

    /**
     * Translates the specified byte array into an "alternate representation" Base64 string. This non-standard variant
     * uses an alphabet that does not contain the uppercase alphabetic characters, which makes it suitable for use in
     * situations where case-folding occurs.
     *
     * @param bytes data
     * @return base64
     */
    public static byte[] encodeAltBase64(byte[] bytes) {
        return encode(bytes, ALT_ENCODE_TABLE);
    }

    /**
     * encode usl-safe-base64
     *
     * @param bytes data
     * @return base64
     */
    public static byte[] encodeUrlSafeBase64(byte[] bytes) {
        return encode(bytes, URL_SAFE_ENCODE_TABLE);
    }

    /**
     * encode data
     *
     * @param bytes       data
     * @param encodeTable table
     * @return base64
     */
    private static byte[] encode(byte[] bytes, byte[] encodeTable) {
        if (null == bytes) {
            return null;
        }
        int len = bytes.length;
        if (len == 0) {
            return new byte[0];
        }
        int evenlen = (len / 3) * 3;
        int destlen = ((len - 1) / 3 + 1) << 2;
        byte[] dest = new byte[destlen];
        for (int s = 0, d = 0; s < evenlen; ) {
            int i = (bytes[s++] & 0xff) << 16 | (bytes[s++] & 0xff) << 8 | (bytes[s++] & 0xff);

            dest[d++] = encodeTable[(i >>> 18) & 0x3f];
            dest[d++] = encodeTable[(i >>> 12) & 0x3f];
            dest[d++] = encodeTable[(i >>> 6) & 0x3f];
            dest[d++] = encodeTable[i & 0x3f];
        }
        int left = len - evenlen;
        if (left > 0) {
            int i = ((bytes[evenlen] & 0xff) << 10) | (left == 2 ? ((bytes[len - 1] & 0xff) << 2) : 0);
            dest[destlen - 4] = encodeTable[i >> 12];
            dest[destlen - 3] = encodeTable[(i >>> 6) & 0x3f];
            dest[destlen - 2] = (left == 2) ? encodeTable[i & 0x3f] : (byte) '=';
            dest[destlen - 1] = '=';
        }
        return dest;
    }

    /**
     * Translates the specified Base64 string (as per Preferences.get(byte[])) into a byte array.
     *
     * @param base64 base64
     * @return data
     */
    public static byte[] decodeAltBase64(byte[] base64) {
        return decode(base64, ALT_DECODE_TABLE);
    }

    /**
     * decode url-safe-base64
     *
     * @param base64 base64
     * @return data
     */
    public static byte[] decodeUrlSafeBase64(byte[] base64) {
        return decode(base64, URL_SAFE_DECODE_TABLE);
    }

    /**
     * Translates the specified "alternate representation" Base64 string into a byte array.
     *
     * @param base64 base64
     * @return data
     */
    public static byte[] decode(byte[] base64) {
        return decode(base64, DECODE_TABLE);
    }

    /**
     * decode base64 string
     *
     * @param base64 base64
     * @return data
     */
    public static byte[] decodeString(String base64) {
        return decode(base64.getBytes(), DECODE_TABLE);
    }

    /**
     * decode data
     *
     * @param base64      base64
     * @param encodeTable encodeTable
     * @return decode data
     */
    public static byte[] decode(byte[] base64, byte[] encodeTable) {
        int sLen = base64.length;
        int numGroups = sLen / 4;
        if (4 * numGroups != sLen) {
            throw new IllegalArgumentException("String length must be a multiple of four.");
        }
        int missingBytesInLastGroup = 0;
        int numFullGroups = numGroups;
        if (sLen != 0) {
            if (base64[sLen - 1] == '=') {
                missingBytesInLastGroup++;
                numFullGroups--;
            }
            if (base64[sLen - 2] == '=') {
                missingBytesInLastGroup++;
            }
        }
        byte[] result = new byte[3 * numGroups - missingBytesInLastGroup];

        // Translate all full groups from base64 to byte array elements
        int inCursor = 0, outCursor = 0;
        byte ch0, ch1, ch2, ch3;
        for (int i = 0; i < numFullGroups; i++) {
            ch0 = base64toInt(base64[inCursor++], encodeTable);
            ch1 = base64toInt(base64[inCursor++], encodeTable);
            ch2 = base64toInt(base64[inCursor++], encodeTable);
            ch3 = base64toInt(base64[inCursor++], encodeTable);
            result[outCursor++] = (byte) ((ch0 << 2) | (ch1 >> 4));
            result[outCursor++] = (byte) ((ch1 << 4) | (ch2 >> 2));
            result[outCursor++] = (byte) ((ch2 << 6) | ch3);
        }

        // Translate partial group, if present
        if (missingBytesInLastGroup != 0) {
            ch0 = base64toInt(base64[inCursor++], encodeTable);
            ch1 = base64toInt(base64[inCursor++], encodeTable);
            result[outCursor++] = (byte) ((ch0 << 2) | (ch1 >> 4));

            if (missingBytesInLastGroup == 1) {
                ch2 = base64toInt(base64[inCursor++], encodeTable);
                result[outCursor++] = (byte) ((ch1 << 4) | (ch2 >> 2));
            }
        }
        // assert inCursor == s.length()-missingBytesInLastGroup;
        // assert outCursor == result.length;
        return result;
    }

    /**
     * Translates the specified character, which is assumed to be in the "Base 64 Alphabet" into its equivalent 6-bit
     * positive integer.
     *
     * @throws IllegalArgumentException IllegalArgumentException or ArrayOutOfBoundsException if c is not in the Base64 Alphabet.
     */
    private static byte base64toInt(byte c, byte[] alphaToInt) {
        byte result = alphaToInt[c];
        if (result < 0) {
            throw new IllegalArgumentException("Illegal character " + c);
        }
        return result;
    }

    /**
     * 解码表
     * <p>
     * RFC 4648 和 RFC 2045 都可以使用这个解码表
     * <p>
     * This array is a lookup table that translates unicode characters drawn from the "Base64 Alphabet" (as specified in
     * Table 1 of RFC 2045) into their 6-bit positive integer equivalents. 128 bits in total. Characters that are not in the Base64
     * alphabet but fall within the bounds of the array are translated to -1.
     * 0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
     * -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
     * -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f
     * -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, // 20-2f + - /
     * 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, // 30-3f 0-9
     * -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, // 40-4f A-O
     * 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, // 50-5f P-Z _
     * -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o
     * 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51                      // 70-7a p-z
     */
    private static final byte[] DECODE_TABLE = {
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
            52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
            -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
            -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
            41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51};

    /**
     * 不受大小写影响的 Base64
     * <p>
     * 对应的解码表
     * <p>
     * This array is the analogue of base64ToInt, but for the nonstandard variant that avoids the use of uppercase
     * alphabetic characters.
     */
    private static final byte[] ALT_DECODE_TABLE = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, 62, 9, 10,
            11, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 12, 13, 14, -1, 15, 63, 16, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 17, -1, 18, 19, 21, 20, 26, 27, 28,
            29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 22, 23, 24, 25};


    private static final byte[] URL_SAFE_DECODE_TABLE = DECODE_TABLE;

    /**
     * URL 安全的 Base64（RFC 4648）
     * <p>
     * 将 encode-table 中的 + 和 / 替换为 - 和 _ ，并移除填充 =。
     * <p>
     * URL-safe decode table，replace '+','/' as '-', '_'
     */
    private static final byte[] URL_SAFE_ENCODE_TABLE = {
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
            'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
            'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
            'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
            'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
            'w', 'x', 'y', 'z', '0', '1', '2', '3',
            '4', '5', '6', '7', '8', '9', '-', '_'
    };

    /**
     * 标准的 Base64（RFC 2045）
     * <p>
     * This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet"
     * equivalents as specified in Table 1 of RFC 2045.
     */
    private static final byte[] ENCODE_TABLE = {
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
            'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
            'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
            'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
            'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
            'w', 'x', 'y', 'z', '0', '1', '2', '3',
            '4', '5', '6', '7', '8', '9', '+', '/'};

    /**
     * 不受大小写影响的 Base64
     * <p>
     * This array is a lookup table that translates 6-bit positive integer index values into their
     * "Alternate Base64 Alphabet" equivalents. This is NOT the real Base64 Alphabet as per in Table 1 of RFC 2045. This
     * alternate alphabet does not use the capital letters. It is designed for use in environments where "case folding"
     * occurs.
     */
    private static final byte[] ALT_ENCODE_TABLE = {
            '!', '"', '#', '$', '%', '&', '\'', '(',
            ')', ',', '-', '.', ':', ';', '<', '>',
            '@', '[', ']', '^', '`', '_', '{', '|',
            '}', '~', 'a', 'b', 'c', 'd', 'e', 'f',
            'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
            'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
            'w', 'x', 'y', 'z', '0', '1', '2', '3',
            '4', '5', '6', '7', '8', '9', '+', '?'};
}